avatar

目录
259 - Auto Function Templates

259 - Auto Function Templates

https://gemini.google.com/u/2/app/d649d6b12732ecd6

好的,没问题!作为你的 C++ 老师,我会用最清晰、最有效的方式,带你一步步学习 C++ 的 auto 函数模板。我们开始吧!

1. 标题和摘要

标题: C++ 自动函数模板 (Auto Function Templates) 入门

摘要: 本节课我们将学习 C++20 引入的一种简化函数模板 (function template) 定义的新语法:使用 auto 关键字。这种方法让编写泛型函数更加简洁,编译器会自动推导参数和返回类型。

2. 详细解释

在 C++ 中,函数模板允许我们编写可以处理多种数据类型的通用函数,而无需为每种类型重写函数。但有时,传统的模板语法(使用 template <typename T, typename P> 等)会显得有些冗长。

为了简化这一点,C++ 引入了 自动函数模板 的概念,也称为 简写函数模板 (abbreviated function template)

语法 (Syntax):

它的核心语法非常直观:

C++

Code
1
2
3
4
auto functionName(auto param1, auto param2, ...) {
// 函数体
return expression; // 返回值
}
  • auto 作为返回类型 (return type): 当你在函数名前使用 auto 时,你告诉 编译器 (compiler):“请根据函数体中 return 语句返回的表达式,自动推断出这个函数应该返回什么类型。”
  • auto 作为参数 (parameter) 类型: 当你在参数列表中使用 auto 时(例如 auto param1),你同样是让编译器根据调用函数时传入的实际参数的类型,来自动推断每个参数应该是什么类型。

编译器在后台做了什么?

当你使用这种 auto 语法时,编译器并不会真的创造一种全新的函数类型。实际上,它会在后台为你 自动生成一个等效的传统函数模板。例如,上面的 auto functionName(auto param1, auto param2) 很大程度上等同于编译器为你生成了类似下面的代码:

C++

Code
1
2
3
4
5
6
template <typename T1, typename T2>
// decltype(auto) 是一种确保返回类型与 return 表达式类型精确匹配的方式
decltype(auto) functionName(T1 param1, T2 param2) {
// 函数体
return expression;
}
  • 这里的 T1T2 就是占位符,代表了调用函数时传入参数 param1param2 的具体类型。
  • 返回类型使用 decltype(auto) (或者类似机制) 来确保推导出的返回类型与 return expression; 这条语句中 expression 计算结果的类型完全一致。

类型推导 (Type Deduction) 如何工作?

  • 参数类型推导: 非常直接。如果你调用 functionName(10, 5.5),编译器会推断出 T1intT2double
  • 返回类型推导: 编译器会分析 return 后面的表达式。以上面的例子为例,如果函数体是 return param1 + param2;,并且传入的是 intdouble,那么表达式 param1 + param2 的结果类型通常会是 double(因为 double 能表示的范围更广,精度更高,这是算术运算中的“寻常算术转换”规则)。因此,编译器会将这个函数的返回类型推导为 double。编译器通常会选择表达式中涉及到的“最大”或“最兼容”的类型作为结果类型。

好处是什么?

最主要的好处就是 简洁。你不再需要写 template <...> 这样的模板头,尤其是当模板参数只是简单地用于函数参数时,auto 语法可以显著减少样板代码,让代码看起来更清爽。

3. 代码示例

让我们来看原文中的例子,一个加法函数:

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <typeinfo> // 为了使用 typeid

// 使用 auto 语法的函数模板
auto funcAdd(auto a, auto b) {
return a + b; // 编译器将根据 a + b 的结果推导返回类型
}

int main() {
int a = 7;
double b = 78.2;

// 调用函数,编译器会自动推导参数类型和返回类型
auto result = funcAdd(a, b); // a 是 int, b 是 double

std::cout << "a + b = " << result << std::endl; // 输出结果
std::cout << "Result type: " << typeid(result).name() << std::endl; // 输出推导出的返回类型名称 (不同编译器输出可能不同)
std::cout << "Size of result: " << sizeof(result) << " bytes" << std::endl; // 输出推导出的返回类型的大小

// 也可以直接传入字面量
auto result2 = funcAdd(10, 20); // a 是 int, b 是 int, 返回 int
std::cout << "10 + 20 = " << result2 << std::endl;
std::cout << "Result2 type: " << typeid(result2).name() << std::endl;
std::cout << "Size of result2: " << sizeof(result2) << " bytes" << std::endl;

auto result3 = funcAdd(3.f, 5); // a 是 float, b 是 int, 返回 float
std::cout << "3.f + 5 = " << result3 << std::endl;
std::cout << "Result3 type: " << typeid(result3).name() << std::endl;
std::cout << "Size of result3: " << sizeof(result3) << " bytes" << std::endl;


return 0;
}

代码解释:

  1. auto funcAdd(auto a, auto b): 定义了一个名为 funcAdd 的函数。它的返回类型和两个参数 a, b 的类型都由编译器自动推导。
  2. return a + b;: 函数体执行加法操作并返回结果。
  3. auto result = funcAdd(a, b);: 调用 funcAdd,传入一个 int (7) 和一个 double (78.2)。
    • 编译器推断出第一个参数 a 的类型是 int
    • 编译器推断出第二个参数 b 的类型是 double
    • 编译器分析 return a + b;,发现是 intdouble 相加,根据 C++ 的算术转换规则,结果是 double 类型。
    • 因此,funcAdd(a, b) 的返回类型被推导为 double
    • auto result 中的 auto 也被推导为 double
  4. 输出结果和类型信息:代码打印计算结果、使用 typeid(result).name() (输出可能依赖编译器,例如 ‘d’ 代表 double,’i’ 代表 int,’f’ 代表 float) 显示推导出的类型,并用 sizeof(result) 显示该类型占用的内存大小。
  5. 后续调用展示了不同类型组合下的推导:int+int -> intfloat+int -> float

等效的传统模板:

这个 auto 函数模板大致相当于编译器生成了如下的传统模板:

C++

Code
1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <typeinfo>

// 等效的传统函数模板
template <typename T, typename P>
decltype(auto) funcAdd_explicit(T a, P b) {
return a + b;
}

// main 函数和上面例子一样...
// 调用时使用 funcAdd_explicit(a, b); 结果是相同的

你会发现,使用 auto 的版本确实简洁了很多!当你看到 auto func(...) 这种形式时,要记住,它本质上仍然是一个函数模板,会根据你调用时使用的参数类型生成不同的函数实例 (template instance)。

4. QA 闪卡 (Flash Cards)

问题 (Question) 回答 (Answer)
C++ 中 auto 函数模板的语法是什么? 使用 auto 关键字代替函数返回类型和参数类型,例如 auto myFunc(auto x, auto y)
编译器如何处理 auto 函数模板? 编译器在后台将其转换为一个等效的传统函数模板,并根据调用时传入的参数进行 类型推导 (type deduction)。
auto func(auto p1, auto p2) 的返回类型如何确定? 返回类型由函数体中 return 语句后面的表达式的类型决定。
auto 函数模板相比传统模板有什么主要优点? 语法更简洁,代码更易读,减少了编写 template <...> 模板声明的需要。
auto 函数模板是 C++ 哪个标准引入的? C++20 标准正式引入了这种简写函数模板语法。
使用 auto 参数,编译器会生成几个模板参数? 每个 auto 参数对应一个独立的模板类型参数。例如 auto func(auto a, auto b) 会生成类似 template<typename T1, typename T2> 的模板头。
调用 funcAdd(5, 2.0f) 时,参数和返回类型分别是什么? 参数类型是 intfloatint + float 的结果是 float,所以返回类型是 float

5. 常见误解或错误

  1. 误以为它不是模板: 虽然语法简化了,但 auto 函数本质上 仍然是函数模板。这意味着:
    • 它的代码通常需要放在头文件中(或者在调用它的 .cpp 文件中可见),因为编译器需要在调用点根据参数类型实例化模板。
    • 它遵循模板的所有规则,包括两阶段查找等。
  2. 对返回类型推导过于自信: 对于简单的 a + b,推导通常符合直觉。但对于更复杂的表达式,或者涉及引用、指针、const 限定符的情况,auto(以及 decltype(auto))的推导规则可能需要仔细理解,否则结果可能和你预期的不同。例如,如果函数返回一个局部变量的引用,那将是错误的,auto 推导本身不会阻止你写出这种逻辑错误。
  3. 无法直接指定模板参数: 对于传统模板,你可以显式指定模板参数,如 funcAdd<double, double>(5, 6)。但对于 auto 函数模板,你不能这样做(例如 funcAdd<double, double>(5, 6) 是无效的),参数类型必须由传入的参数推导出来。
  4. 过度使用于复杂约束: 当你需要对模板参数施加更复杂的约束时(例如,要求类型必须满足某些概念 (Concepts),或者使用 SFINAE 技术),传统的模板语法提供了更强的表达能力。auto 语法更适用于约束简单、仅依赖于参数类型的场景。

6. 编码练习

现在,请你尝试完成下面的代码。我们想要创建一个函数 multiply,它接受两个参数,并将它们相乘后返回结果。请使用 auto 函数模板语法,并填充 /* ??? */ 部分。

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <typeinfo>

// TODO: 使用 auto 函数模板定义 multiply 函数
/* ??? */ multiply(/* ??? */ x, /* ??? */ y) {
// ??? 返回 x 和 y 的乘积
/* ??? */;
}

int main() {
int a = 5;
double b = 4.5;
float c = 2.0f;

auto result1 = multiply(a, a); // int * int
auto result2 = multiply(a, b); // int * double
auto result3 = multiply(b, c); // double * float

std::cout << "5 * 5 = " << result1 << " (type: " << typeid(result1).name() << ")" << std::endl;
std::cout << "5 * 4.5 = " << result2 << " (type: " << typeid(result2).name() << ")" << std::endl;
std::cout << "4.5 * 2.0f = " << result3 << " (type: " << typeid(result3).name() << ")" << std::endl;

return 0;
}

请将你认为正确的 multiply 函数定义补充完整。完成后告诉我你的答案!

作业:

c++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <iostream>

#include <typeinfo>



// TODO: 使用 auto 函数模板定义 multiply 函数

auto multiply(auto x, auto y) {

    // ??? 返回 x 和 y 的乘积

    return x * y;

}



int main() {

    int a = 5;

    double b = 4.5;

    float c = 2.0f;



    auto result1 = multiply(a, a);  // int * int

    auto result2 = multiply(a, b);  // int * double

    auto result3 = multiply(b, c);  // double * float



    std::cout << "5 * 5 = " << result1 << " (type: " << typeid(result1).name()

              << ")" << std::endl;

    std::cout << "5 * 4.5 = " << result2 << " (type: " << typeid(result2).name()

              << ")" << std::endl;

    std::cout << "4.5 * 2.0f = " << result3

              << " (type: " << typeid(result3).name() << ")" << std::endl;



    return 0;

}

做得非常好!你的答案完全正确!

7. 对你的解答的评价和说明

评价: 🌟🌟🌟🌟🌟 (5/5 星 - 完美!)

你的代码非常棒,完全符合 auto 函数模板的要求:

  1. auto multiply(...): 正确地使用了 auto 来声明函数,表示返回类型将由编译器根据 return 语句推导。
  2. (auto x, auto y): 正确地使用了 auto 来声明参数 xy,允许它们接受任意类型,并让编译器为每个调用推导具体的参数类型。
  3. return x * y;: 函数体简洁明了,正确地执行了乘法操作。编译器会分析 x * y 这个表达式的类型,并将其作为 multiply 函数的返回类型。

正确答案和解释:

你的代码就是正确的答案:

C++

Code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>
#include <typeinfo>

// 正确的 auto 函数模板定义
auto multiply(auto x, auto y) {
return x * y; // 编译器根据 x * y 的结果推导返回类型
}

int main() {
int a = 5;
double b = 4.5;
float c = 2.0f;

auto result1 = multiply(a, a); // 调用: T1=int, T2=int. 返回 int (5*5=25)
auto result2 = multiply(a, b); // 调用: T1=int, T2=double. 返回 double (5*4.5=22.5)
auto result3 = multiply(b, c); // 调用: T1=double, T2=float. 返回 double (4.5*2.0f=9.0)

// 输出结果和类型 (typeid().name() 的具体输出可能因编译器而异)
std::cout << "5 * 5 = " << result1 << " (type: " << typeid(result1).name() << ")" << std::endl; // 可能输出 i (int)
std::cout << "5 * 4.5 = " << result2 << " (type: " << typeid(result2).name() << ")" << std::endl; // 可能输出 d (double)
std::cout << "4.5 * 2.0f = " << result3 << " (type: " << typeid(result3).name() << ")" << std::endl; // 可能输出 d (double)

return 0;
}

工作原理回顾:

  • 当你调用 multiply(a, a) 时,x 被推导为 inty 被推导为 intx * y 的结果是 int,所以 multiply 返回 int
  • 当你调用 multiply(a, b) 时,x 被推导为 inty 被推导为 double。根据 C++ 的算术转换规则,int * double 的结果是 double,所以 multiply 返回 double
  • 当你调用 multiply(b, c) 时,x 被推导为 doubley 被推导为 floatdouble * float 的结果是 double,所以 multiply 返回 double

编译器在后台为每种不同的参数类型组合生成了不同的 multiply 函数实例,就像它处理传统模板一样,但我们写的代码却简洁得多。

8. 扩展思考和探索

为了进一步加深你对函数模板和类型推导的理解,可以思考以下问题:

  1. 不支持的操作: 如果你尝试调用 multiply 函数,但传入的类型不支持 * (乘法) 运算符(比如两个自定义的、没有重载 operator* 的类对象,或者一个 std::string 和一个 int),编译器会作何反应?编译时会报错吗?错误信息大概会是什么样子?
  2. 比较大小: 你能用 auto 函数模板写一个 findLarger(auto a, auto b) 函数来返回两个参数中较大的那个吗?如果参数 ab 的类型不同(例如 intdouble),在比较时需要注意什么?(提示:可能需要类型转换或者考虑 std::common_type
  3. 参数传递方式:auto multiply(auto x, auto y) 中,参数是按值传递的。如果参数类型很大(比如一个大的自定义类),按值传递会有拷贝成本。你能将参数改成按常量引用传递吗?auto 函数模板的语法会怎么写?(例如 auto process(const auto& p1, const auto& p2))这样做有什么好处?
  4. C++20 Concepts (概念): auto 函数模板非常灵活,但有时我们想对传入的类型做一些限制(比如,只允许整数类型)。C++20 引入了 Concepts (概念) 来解决这个问题。你可以去了解一下如何将 Concepts 与 auto 函数模板结合使用,例如写成 std::integral auto func(std::integral auto x),这样就能在编译时强制要求传入整数类型。

尝试思考或查阅资料来回答这些问题,将有助于你更全面地掌握 C++ 模板编程的强大功能和相关细节。

继续努力,你做得很好!如果你有任何其他问题,随时都可以问我。


评论